Skip to content

feat(identity): database overlay for federation identity (#326 Phase 1)#338

Merged
TemujinCalidius merged 1 commit into
devfrom
feat/identity-db-overlay
Jul 23, 2026
Merged

feat(identity): database overlay for federation identity (#326 Phase 1)#338
TemujinCalidius merged 1 commit into
devfrom
feat/identity-db-overlay

Conversation

@TemujinCalidius

Copy link
Copy Markdown
Owner

Phase 1 of #326, on top of Phase 0 (#337). Read path only — nothing writes the identity.* rows yet, so every instance still resolves from the environment exactly as before.

What

Resolution becomes runtime override → environment → built-in default.

The overlay lives in a new server-only identity-store.ts, not in identity.ts. That's forced: site.config.ts imports identity.ts, and site.config.ts reaches client bundles — a prisma import there would pull the database client into the browser. The store pushes loaded values into the accessor instead.

That design keeps getIdentity() synchronous, which matters more than it looks: ~60 call sites read it, several from sync helpers, and making it async would have meant converting every one of them — churn on the federation-critical path for no benefit, since identity effectively never changes.

Loaded once at boot, and awaited

instrumentation.ts awaits loadIdentity() before the scheduler starts.

Not per-request, and not on a TTL: a synchronous accessor can't refresh itself, and a stale identity is far worse than a stale setting elsewhere. A request served mid-load would sign with the environment's identity rather than the configured one — the same silent actor-id mismatch this area exists to prevent — so the load is awaited rather than fired and forgotten.

Safety properties (all tested)

Situation Behaviour Why it's the safe direction
Row missing for one field Falls through to environment, not to a default The alternative is an instance quietly federating as @me@localhost:3000
Junk value (blank, whitespace, over-long) Ignored, environment used A malformed actor id is undetectable from the inside
Database down / mid-migration Boots on the environment identity Refusing to start is worse than starting correctly-as-before
Reload fails after a successful load Drops the override Serving a value we can no longer confirm is the wrong bet
Override IS active Logs loudly with the resolved address "The identity isn't what .env.local says" is the first thing you'd want when debugging
Derived values (actorId, keyId, webfingerSubject) Follow the override together They must never straddle two sources

Also: documented what a domain change really involves

docs/fediverse-setup.md gains Changing Your Domain, and it's blunt because the constraint is:

ActivityPub has no rename. The network's answer is an alias (alsoKnownAs) on the new account plus a Move from the old — and remote servers verify it by fetching both ends. So the old domain has to still be serving while followers migrate. Lose the domain first and your followers cannot be brought across by any later action — you keep who you follow (that's your own database), but the people following you are gone.

FediHome implements none of that handshake yet, so the honest guidance today is to treat the domain as permanent, plus the shape of a move that does work: new instance alongside the old, publish the move, leave the old server up for weeks, decommission last.

Verification

Check Result
npx tsc --noEmit clean
npx eslint . 0 errors
npx vitest run 690 passed (74 files) — +9
npm run build clean
changelog sync

Behaviour on any existing instance is unchanged — with no identity.* rows, getIdentity() resolves precisely as it did before.

Not in this PR

The write path, the admin UI, and the change-domain migration. Two constraints are recorded in the code for whoever builds them: the load is process-local (a pm2 cluster refreshes only the worker that handled the write — a partial rollout of a federation identity across workers is exactly the silent mismatch to avoid), and Move support has to exist before a domain change can keep followers at all.

🤖 Generated with Claude Code

Identity now resolves runtime override -> environment -> default. Nothing
writes the identity.* rows yet, so every instance still resolves from the
environment exactly as before; this is the read path and its safety properties,
landed on its own so the write path has something proven to build on.

The overlay lives in a new server-only identity-store.ts rather than in
identity.ts, because site.config.ts imports identity.ts and that reaches client
bundles — a prisma import there would pull the database client into the browser.
The store pushes loaded values into the accessor instead, which keeps
getIdentity() synchronous. That matters: roughly 60 call sites read it, several
from sync helpers, and making it async would have meant converting all of them.

Loaded once at boot from instrumentation.ts, awaited before the scheduler
starts. Not per-request and not on a TTL: identity effectively never changes, a
stale value here is far worse than a stale setting elsewhere, and a synchronous
accessor cannot refresh itself anyway. A request served mid-load would sign with
the environment's identity instead of the configured one — the same silent
actor-id mismatch this whole area exists to prevent — so the load is awaited
rather than fired off.

Safety properties, all tested: a missing row falls through to the environment
rather than to a default (the dangerous alternative is an instance quietly
federating as @me@localhost:3000); junk values are rejected instead of building
a malformed actor id; a database that is down or mid-migration leaves the
instance on its environment identity rather than refusing to boot; and a failed
reload drops a previously-loaded override rather than serving it blind. An
override that IS in effect logs loudly, since "the identity isn't what
.env.local says" is the first thing worth knowing when debugging federation.

Also documents domain changes in docs/fediverse-setup.md. ActivityPub has no
rename, and the alias + Move handshake only works while the OLD domain is still
serving, because remote servers verify by fetching both ends. Lose the domain
first and followers cannot be migrated by any later action. FediHome implements
none of that handshake yet, so the honest guidance is to treat the domain as
permanent, and the shape of a move that does work is written down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TemujinCalidius
TemujinCalidius merged commit c6bb237 into dev Jul 23, 2026
6 checks passed
@TemujinCalidius
TemujinCalidius deleted the feat/identity-db-overlay branch July 23, 2026 09:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant